﻿using System;
using System.Data;
using System.Text;
using System.Collections;

namespace BizElements.Core
{
    /// <summary>Enumerates supported update/set expression type.</summary>
    public enum UpdateExpressionType
    {
        /// <summary>An expression that sets a provider value/paremeter to target column.</summary>
        Value,
        /// <summary>An expression that sets a NULL value to target column.</summary>
        Null,
        /// <summary>An expression that copies a value from other column to target column.</summary>
        OtherColumn
    }

    /// <summary>
    /// Represents a column which is to be changed and an expression that computes/contains the new value.	
    /// </summary>
    /// <remarks><see cref="UpdateExpression"/> objects may be instantiated only by <see cref="UpdateExpressionFactory"/> class.
    /// Constructors are not public.</remarks>
    [Serializable]
    public sealed class UpdateExpression : ISqlItem, IRenderSql
    {
        IDbColumn column;
        IDbColumn[] sourceColumns;

        #region Constructors.

        private UpdateExpression()
        {
            this.DataType = typeof(object);
            this.DbType = DbType.Object;            
        }

        private UpdateExpression(object item, SqlItemType itemType)
            : this()
        {
            this.Item = item;
            this.ItemType = itemType;
        }

        private UpdateExpression(object parameterValue, DbType parameterType, int size, string parameterNamePrefix)
        {
            // Unique parameter ID is automatically appended to parameter name during the rendering phase.
            // Eg: 'parameter_15'. This is a safety precaution. It is possible that there are more
            // parameters with the same name.
            DbParameter dbpar = new DbParameter();
            dbpar.ParameterName = SqlItemUtil.CleanParameterName(parameterNamePrefix);
            dbpar.Direction = ParameterDirection.Input;
            dbpar.DbType = parameterType;
            dbpar.Value = parameterValue;
            if (size > 0)
                dbpar.Size = size;

            this.Item = dbpar;
            this.ItemType = SqlItemType.Parameter;
            this.DataType = TypeUtil.NetTypeFrom(parameterType);
            this.DbType = parameterType;
        }

        /// <summary>
        /// Creates a new SET expression.
        /// </summary>
        /// <param name="expressionType">Expression type.</param>
        /// <param name="column">Column which is to be changed.</param>
        /// <param name="sourceColumns">Columns required by the expression to compute the new value.
        /// Required to properly render the FROM clause when columns from other tables are used. 
        /// <b>Null</b> if the expression doesn't use any columns.</param>
        /// <param name="item">SQL item that computes/contains the new value.</param>
        /// <param name="itemType">Item type.</param>
        internal UpdateExpression(UpdateExpressionType expressionType, IDbColumn column, DbColumnCollection sourceColumns, object item, SqlItemType itemType)
            : this(item, itemType)
        {
            this.ExpressionType = expressionType;
            this.column = column;
            if (sourceColumns == null || sourceColumns.Count == 0)
                this.sourceColumns = new IDbColumn[0];
            else
                this.sourceColumns = sourceColumns.ToArray();
        }

        /// <summary>
        /// Creates a new SET expression that sets the given value.
        /// Throws exception if the value is <b>null</b>.
        /// </summary>
        /// <param name="column">Column which is to be changed.</param>
        /// <param name="value">New value. Cannot be <b>null</b>.</param>
        internal UpdateExpression(IDbColumn column, object value) :
            this(value, column.DbType, column.MaxLengthIfText, column.GetPrefixedAlias())
        {
            if (value == null)
                throw new ArgumentNullException("value", Messages.UpdateExpression_ValMayNotBeNull);

            this.ExpressionType = UpdateExpressionType.Value;
            this.column = column;
            this.sourceColumns = new IDbColumn[0];
        }

        #endregion

        #region Methods.

        /// <summary>
        /// Renders this expression.
        /// </summary>
        /// <param name="dbms">Target DBMS.</param>
        /// <param name="output">StringBuilder to which SQL is appended.</param>
        /// <param name="parameters">SQL parameter collection to which the object's and its children's
        /// parameters are added. After the rendering is done the collection contains all parameters with unique names.</param>
        public void Render(DbmsType dbms, StringBuilder output, DbParameterCollection parameters)
        {
            this.column.RenderColumnName(dbms, output);
            output.Append(" = ");
            switch (this.ItemType)
            {
                case SqlItemType.Column:
                    IDbColumn col = (IDbColumn)this.Item;
                    col.RenderFullName(dbms, output);
                    break;

                case SqlItemType.Parameter:
                    DbParameter par = (DbParameter)this.Item;
                    par.Render(dbms, output, parameters);
                    break;

                case SqlItemType.SubQuery:
                    SelectStatement select = (SelectStatement)this.Item;
                    select.Render(dbms, output, parameters);
                    break;

                case SqlItemType.FreeText:
                    output.Append((string)this.Item);
                    break;

                case SqlItemType.Function:
                    Function func = (Function)this.Item;
                    func.Render(dbms, output, parameters);
                    break;

                default:
                    throw new ArgumentException(Messages.UpdateExpression_UnsupportedItemType + this.ItemType.ToString());
            }
        }

        #endregion

        #region ISqlItem Properties.

        /// <summary>
        /// Gets contained item.
        /// </summary>
        public object Item { get; private set; }

        /// <summary>
        /// Gets item type.
        /// </summary>
        public SqlItemType ItemType { get; private set; }

        /// <summary>
        /// Gets the SQL data type. DbType.Object if unknown.
        /// </summary>
        public DbType DbType { get; private set; }

        /// <summary>
        /// Gets .NET type. Object if unknown.
        /// </summary>
        public Type DataType { get; private set; }

        /// <summary>
        /// Gets the item's alias/name. Always <b>null</b>.
        /// </summary>
        public string Alias
        {
            get { return null; }
        }

        #endregion

        #region Added Properties.

        /// <summary>
        /// Gets the column that contains the will be updated.
        /// </summary>
        public IDbColumn Column
        {
            get { return this.column; }
        }

        /// <summary>
        /// Gets the columns required by the expression to compute the new value.
        /// Empty (length == 0) if no source columns are required.
        /// </summary>
        public IDbColumn[] SourceColumns
        {
            get { return this.sourceColumns; }
        }

        /// <summary>
        /// Gets the expression type.
        /// </summary>
        public UpdateExpressionType ExpressionType { get; private set; }

        #endregion
    }    
}